This project presents a comprehensive statistical analysis of urban development in Philadelphia, focusing on the intricate balance between growth, affordability, and gentrification. Central to this study is the development of a random forest model, which has demonstrated remarkable effectiveness in predicting future development patterns with a low mean absolute error. By accurately forecasting where growth is likely to occur, this model serves as a critical tool for urban planners and policymakers. It can be strategically leveraged to promote proactive upzoning of high-priority parcels, aligning current zoning more closely with anticipated development. This approach is particularly aimed at fostering affordable housing in Philadelphia, addressing one of the city’s most pressing challenges. Through this blend of data-driven insights and targeted policy recommendations, the project seeks to guide Philadelphia towards a more equitable and sustainable urban future.
Philadelphia, the sixth-largest city in the United States, presents a unique case study in urban development. Despite its ranking as the 42nd in terms of cost of living, the city’s approach to housing supply and affordability stands out. In the complex landscape of urban growth, Philadelphia treads a precarious line, balancing the benefits of new construction with the challenges of affordability and gentrification. This statistical analysis aims to delve into these dynamics, examining how the city’s growth, often perceived as a political rather than a strategic or ‘smart’ process, impacts its residents and neighborhoods.
The citywide impact of development in Philadelphia suggests a positive trend towards increased affordability and choice for its inhabitants. However, this macro view masks the localized burdens of development, where the cost and impact of new construction can be disproportionately felt. The central question then arises: How can Philadelphia grow equitably? The answer may lie in the concept of ‘smart growth’, a strategic approach to urban planning. But in reality, growth in Philadelphia is more influenced by political forces than by comprehensive, well-thought-out plans. Comprehensive plans, ideally set on ten-year timelines, often become mere suggestions, subject to the whims of city council members rather than serving as steadfast guides for development.
The statistical analysis will further explore the critical issue of zoning mismatch, a barrier to ensuring dense and equitable growth in Philadelphia. The current system, where permitting is largely controlled by individual council members (‘council-member-time’), often leads to a situation where exceptions to zoning rules unfairly become the norm. This analysis aims to provide dynamic data and insights to empower zoning advocates, offering them the tools to effectively counter these forces. By highlighting the need for intervention and the reduction of zoning mismatches, the study seeks to contribute to a broader understanding of how Philadelphia can navigate its growth challenges and opportunities, moving towards a more equitable and sustainable urban future.
Predict development pressure: how do we define “a lot of development”?
Define affordability burden: how do we define “affordability burden”? – % change year over year in population that is experience rate burden (will probably see extreme tipping points), growing population, % change in area incomes
Identify problem zoning
Calculate number of connected parcels
Predict development pressure at the block level
Identify not burdened areas
Identify problem zoning
Calcualte number of connected parcels
Advocate for upzoning in parcels where there is local development pressure, no affordability burden, problem zoning, and high number of connected parcels
Make sure to note that we train, test, and then validate. So these first models are based on 2022 data, and then we run another on 2023 (and then predict 2024 at the end).
4.1 OLS Regression
To begin, we run a simple regression incorporating three engineered groups of features: space lag, time lag, and distance to 2022. We include this last variable because of a Philadelphia tax abatement policy that led to a significant increase in residential development in the years immediately before 2022. We will use this as a baseline model to compare to our more complex models.
(We considered a Poisson model but found that it struggled with outliers.)
Show the code
permits_train <-filter(permits_bg %>%select(-c(mapname, geoid10)), year <2022)permits_test <-filter(permits_bg %>%select(-c(mapname, geoid10)), year ==2022)permits_validate <-filter(permits_bg %>%select(-c(mapname, geoid10)), year ==2023)permits_predict <-filter(permits_bg %>%select(-c(mapname, geoid10)), year ==2024)reg <-lm(permits_count ~ ., data =st_drop_geometry(permits_train))predictions <-predict(reg, permits_test)predictions <-cbind(permits_test, predictions)predictions <- predictions %>%mutate(abs_error =abs(permits_count - predictions),pct_error = abs_error / permits_count)ggplot(predictions, aes(x = permits_count, y = predictions)) +geom_point() +labs(title ="Predicted vs. Actual Permits",subtitle ="2022") +geom_smooth(method ="lm", se =FALSE)
We find that our OLS model has an MAE of only MAE: 2.66–not bad for such a simple model! Still, it struggles most in the areas where we most need it to succeed, so we will try to introduce better variables and apply a more complex model to improve our predictions.
ggplot(rf_predictions, aes(x = permits_count, y = rf_predictions)) +geom_point() +labs(title ="Predicted vs. Actual Permits",subtitle ="2022") +geom_smooth(method ="lm", se =FALSE)
We find that error is not related to affordability and actually trends downward with percent nonwhite. (This is probably because there is less total development happening there in majority-minority neighborhoods to begin with, so the magnitude of error is less, even though proportionally it might be more.) Error increases slightly with total pop. This makes sense–more people –> more development.
Show the code
ggplot(rf_predictions, aes(y = abs_error, x = rent_burden)) +# or whatever the variable isgeom_point() +geom_smooth(method ="lm", se=FALSE) +theme_minimal()
Show the code
ggplot(rf_predictions, aes(y = abs_error, x = percent_nonwhite)) +# or whatever the variable isgeom_point() +geom_smooth(method ="lm", se=FALSE) +theme_minimal()
Show the code
ggplot(rf_predictions, aes(y = abs_error, x = total_pop)) +# or whatever the variable isgeom_point() +geom_smooth(method ="lm", se=FALSE) +theme_minimal()
Show the code
ggplot(rf_predictions, aes(y = abs_error, x = med_inc)) +# or whatever the variable isgeom_point() +geom_smooth(method ="lm", se=FALSE) +theme_minimal()
How does this generalize across council districts? Don’t forget to refactor
Show the code
suppressMessages(ggplot(rf_predictions, aes(x =reorder(district, abs_error, FUN = mean), y = abs_error)) +geom_boxplot(fill =NA) +labs(title ="MAE by Council District",y ="Mean Absolute Error",x ="Council District") +theme_minimal() +theme())
5.3 Assessing Upzoning Needs
We can identify conflict between projected development and current zoning.
Look at zoning that is industrial or residential single family in areas that our model suggests are high development risk for 2023:
Furthermore, we can identify properties with high potential for assemblage, which suggests the ability to accomodate high-density, multi-unit housing.
Show the code
nbs <- filtered_zoning %>%mutate(nb =st_contiguity(geometry))# Create edge list while handling cases with no neighborsedge_list <- tibble::tibble(id =1:length(nbs$nb), nbs = nbs$nb) %>% tidyr::unnest(nbs) %>%filter(nbs !=0)# Create a graph with a node for each row in filtered_zoningg <-make_empty_graph(n =nrow(filtered_zoning))V(g)$name <-as.character(1:nrow(filtered_zoning))# Add edges if they existif (nrow(edge_list) >0) { edges <-as.matrix(edge_list) g <-add_edges(g, c(t(edges)))}# Calculate the number of contiguous neighbors, handling nodes without neighborsn_contiguous <-sapply(V(g)$name, function(node) {if (node %in% edges) {length(neighborhood(g, order =1, nodes =as.numeric(node))[[1]]) } else {1# Nodes without neighbors count as 1 (themselves) }})filtered_zoning <- filtered_zoning %>%mutate(n_contig = n_contiguous)filtered_zoning %>%st_drop_geometry() %>%select(rf_predictions, n_contig, OBJECTID, CODE) %>%filter(rf_predictions >10, n_contig >2) %>%arrange(desc(rf_predictions)) %>%kablerize(caption ="Poorly-Zoned Properties with High Development Risk")
Poorly-Zoned Properties with High Development Risk